/* 
Scenic Graphics Library
Copyright (C) 2007 Jouni Tulkki

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USApackage scenic;
*/

package scenic;

import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.ImageCapabilities;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.SampleModel;
import java.awt.image.TileObserver;
import java.awt.image.VolatileImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Map;
import java.util.WeakHashMap;

import scenic.CommandBuffer.CustomCommand;
import scenic.awt.ScenicGraphics2D;

/**
 * <p>The ScenicImage class is used to store images that can be directly used
 * by the Scenic library. This class extends the VolatileImage
 * class, but it also adds several new methods for reading and writing 
 * data. The image data is stored in video memory to make
 * hardware acceleration possible. 
 * 
 * <p>The format of the image data is specified by the format 
 * attribute. Different formats are defined in the Format
 * class.
 */
public class ScenicImage extends VolatileImage {
	static {
		scenic.jni.RenderCanvas.loadLibrary();
	}
	
    protected static BufferedImage dummyImage = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
    protected static Graphics2D dummyGraphics = dummyImage.createGraphics();

    private static Map<Image, ScenicImage> imageCache = 
		new WeakHashMap<Image, ScenicImage>();
	private static long totalMemoryUsage;
	private static long garbageCollectionLimit = 64 * 1024 * 1024;
	
	private int id;
	private int width;
	private int height;
	private int format;
	@SuppressWarnings("unused") // Read from native code
	private int options;
	private LinkedList<ScenicImageObserver> observers = new LinkedList<ScenicImageObserver>();
	
	/** Constructs an empty image
	 */
	public ScenicImage() {
		create(0, 0, 0);
	}
	
	/**
	 * Constructs an image with given parameters. 
	 * 
	 * @param width the width of the image.
	 * @param height the height of the image.
	 * @param format the format of the image (must be one of the 
	 * constants defined in the Format class).
	 */
	public ScenicImage(int width, int height, int format) {
		create(width, height, format);
	}
	
	/**
	 * Constructs an image with given parameters. 
	 * 
	 * @param width the width of the image.
	 * @param height the height of the image.
	 * @param format the format of the image (must be one of the 
	 * constants defined in the Format class).
	 * @param options options for the image (must be a combination
	 * of the constants in ImageOptions class). 
	 */
	public ScenicImage(int width, int height, int format, int options) {
		create(width, height, format, options);
	}
	
//	private static class ImageUpdater implements 
	private int getScenicFormat(int type, ColorModel model) {
		if(type == BufferedImage.TYPE_3BYTE_BGR)
			return Format.R8G8B8;
		if(type == BufferedImage.TYPE_INT_RGB)
			return Format.X8R8G8B8;
		if(type == BufferedImage.TYPE_INT_ARGB)
			return Format.A8R8G8B8;
		if(type == BufferedImage.TYPE_BYTE_GRAY)
			return Format.L8;
		if(type == BufferedImage.TYPE_BYTE_INDEXED)
			return Format.A8R8G8B8;
		if(type == BufferedImage.TYPE_CUSTOM) {
			if(model instanceof DirectColorModel) {
				DirectColorModel m = (DirectColorModel)model;
				
				if(m.getRedMask()    == 0x00ff0000 &&
					m.getGreenMask() == 0x0000ff00 &&
					m.getBlueMask()  == 0x000000ff &&
					m.getAlphaMask() == 0x01000000) {
					return Format.A1R8G8B8;
				}
			}
			throw new IllegalArgumentException("Unsupported custom color model (" + model + ")");
		}
		throw new IllegalArgumentException("Unsupported image type (" + type + ")");
	}
	
	/**
	 * Creates an image from the given BufferedImage. 
	 * 
	 * @param image the source image.
	 */
	public ScenicImage(BufferedImage image) {
//		System.out.println("new ScenicImage from BufferedImage " + image);
		int fmt = getScenicFormat(image.getType(), image.getColorModel());	
		
		create(image.getWidth(), image.getHeight(), fmt);	
		
		writeImage(image);
	}
	
	private int getPixelSize(SampleModel model) {
		int s = model.getNumDataElements();
		
		return s * DataBuffer.getDataTypeSize(model.getDataType()) / 8;
	}
	
	void writeImage(BufferedImage image) {
		WritableRaster raster = image.getRaster();
		DataBuffer buffer = raster.getDataBuffer();
		SampleModel sampleModel = raster.getSampleModel();
		int sampleSize = getPixelSize(sampleModel);
		int offset = (-raster.getSampleModelTranslateX() - 
		 	raster.getSampleModelTranslateY() * sampleModel.getWidth()) * sampleSize;
		int pitch = sampleModel.getWidth() * sampleSize;

//		System.out.println("sample size " + sampleSize + " offset " + offset + " pitch " + pitch);
		
		if(image.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
			IndexColorModel cm = (IndexColorModel)image.getColorModel();
			int w = image.getWidth();
			int h = image.getHeight();
			int colors[] = new int[256];
			int data[] = new int[w * h];
			byte src[] = ((DataBufferByte)buffer).getData();
			
			cm.getRGBs(colors);
			for(int i = 0; i < w * h; i++) {
				data[i] = colors[(int)src[i] & 0xff];
			}
			write(0, 0, width, height, data, Format.A8R8G8B8);	
			return;
		}
		
		
		if(buffer instanceof DataBufferByte) {
			DataBufferByte byteBuffer = (DataBufferByte)buffer;	
			write(0, 0, width, height, byteBuffer.getData(), format, offset,
					pitch);
		}
		else if(buffer instanceof DataBufferInt) {
			DataBufferInt intBuffer = (DataBufferInt)buffer;
			
			write(0, 0, width, height, intBuffer.getData(), format, offset / 4,
					pitch / 4);
		}
		else
			throw new IllegalArgumentException("Unsupported DataBuffer type");		
	}
	
	public static class ImageUpdater implements ImageObserver, TileObserver {
		public Image source;
		
		public ImageUpdater(Image source) {
			this.source = source;
		}
		
		public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
			updateImage();
			return true;
		}

		public void tileUpdate(WritableRenderedImage source, int tileX, int tileY, boolean willBeWritable) {
			updateImage();
		}
		
		private void updateImage() {
			ScenicImage simg = imageCache.get(source);
			
			if(simg != null) {
				simg.writeImage(convertToBufferedImage(source));
			}			
		}
	}
	
	public static BufferedImage convertToBufferedImage(Image img) {
		if(img instanceof BufferedImage) {
			return (BufferedImage)img;				
		} else {
			int w = img.getWidth(null);
			int h = img.getHeight(null);
			BufferedImage bimg;
			
			if(w == 0 || h == 0)
				return null;
			
			bimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);				
			bimg.getGraphics().drawImage(img, 0, 0, null);
			return bimg;
		}
	}
	
	/**
	 * Tries to get a cached ScenicImage that has the content of the
	 * given BufferedImage. If the cache does not contain the desired image
	 * then a new image is created and added to the cache.
	 * 
	 * @param img BufferedImage
	 * @return ScenicImage
	 */
	public static ScenicImage convert(Image img) {
		if(img instanceof ScenicImage)
			return (ScenicImage)img;
		
		ScenicImage simg = imageCache.get(img);
		
		if(simg == null) {
			simg = new ScenicImage(convertToBufferedImage(img));
			Toolkit.getDefaultToolkit().prepareImage(img, -1, -1, new ImageUpdater(img));				
			imageCache.put(img, simg);
		}
		
		return simg;
	}
	
	public static void update(Image img) {
		ScenicImage simg = imageCache.get(img);

		if(simg != null)
			simg.writeImage(convertToBufferedImage(img));
	}
	
	/**
	 * Converts the image to a java.awt.image.BufferedImage object.
	 * The type of the buffered image must be either BufferedImage.TYPE_4BYTE_ABGR
	 * or BufferedImage.TYPE_3BYTE_BGR.
	 * 
	 * @param type type of the image
	 * @return the BufferedImage object
	 */
	public BufferedImage convertToBufferedImage(int type) {
		int pixelFormat;
		
		if(type == BufferedImage.TYPE_3BYTE_BGR)
			pixelFormat = Format.R8G8B8;
		else if(type == BufferedImage.TYPE_4BYTE_ABGR)
			pixelFormat = Format.R8G8B8A8;
		else
			throw new IllegalArgumentException();
			
		
		BufferedImage image = new BufferedImage(width, height, type);
		DataBuffer buffer = image.getRaster().getDataBuffer();
		DataBufferByte byteBuffer = (DataBufferByte)buffer;		
		
		read(0, 0, width, height, byteBuffer.getData(), pixelFormat);
		return image;
	}
	
	/**
	 * Converts the image to a java.awt.image.BufferedImage object.
	 * The type of the buffered image is BufferedImage.TYPE_3BYTE_BGR.
	 * 
	 * @return the BufferedImage object
	 */
	public BufferedImage convertToBufferedImage() {
		return convertToBufferedImage(BufferedImage.TYPE_3BYTE_BGR);
	}
	
	protected void finalize() {
		free();
	}
	
	/**
	 * Gets the width of the image.
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * Gets the height of the image.
	 */
	public int getHeight() {
		return height;
	}
	
	/**
	 * Gets the format of the image. The format is one of the constants
	 * defined in the Format class.
	 */
	public int getFormat() {
		return format;
	}
	
	/**
	 * Creates an image with the given parameters. The new image replaces
	 * the old image. Pixel data is not copied to the new image.
	 * 
	 * @param width the width of the image.
	 * @param height the height of the image.
	 * @param format the format of the image (must be one of the 
	 * constants defined in the Format class).
	 * @param options options for the image (must be one of the 
	 * constants defined in the ImageOptions class).
	 */
	public void create(int width, int height, int format, int options) {
		totalMemoryUsage -= this.width * this.height * Format.getPixelSize(this.format);
		this.width = width;
		this.height = height;
		this.format = format;
		this.options = options;
		id = nativeCreate(id, width, height, format, options);
		markChanged();
		
		/* 
		 * Keep track of total amount of native memory and initiate garbage collection
		 * when the amount of new allocated memory exceeds certain limit. 
		 */
		totalMemoryUsage += width * height * Format.getPixelSize(format);
//		System.out.println("totalMemoryUsage: " + totalMemoryUsage / 1024 / 1024.0);
		if(totalMemoryUsage > garbageCollectionLimit) {
			System.gc();
			totalMemoryUsage = 0;
		}
	}

	/**
	 * Creates an image with the given parameters. The new image replaces
	 * the old image. Pixel data is not copied to the new image.
	 * 
	 * @param width the width of the image.
	 * @param height the height of the image.
	 * @param format the format of the image (must be one of the 
	 * constants defined in the Format class).
	 */
	public void create(int width, int height, int format) {
		create(width, height, format, 0);
	}

	synchronized public void addObserver(ScenicImageObserver observer) {
		observers.add(observer);
	}
	
	synchronized public void deleteObserver(ScenicImageObserver observer) {
		observers.remove(observer);
	}
	
	synchronized private void markChanged() {
//		setChanged();
		for(ScenicImageObserver n : observers)
			n.update(this);
//		clearChanged();		
	}
	
	/**
	 * Checks if the contents of the image are lost. Some image types
	 * are volatile, meaning that their contents can be lost. Images that
	 * are used as render targets can be lost when using DirectX. On OpenGL
	 * images are never lost. For compatibility, you should always check
	 * if the contents are lost when you are using render target images.
	 * 
	 * @return true if the contents of the image are lost
	 */
	public boolean contentsLost() {
		return nativeContentsLost(id);
	}

	int getId() {
		return id;
	}
	
	/**
	 * Frees the resources associated with the image.
	 */
	public void free() {
		totalMemoryUsage -= width * height * Format.getPixelSize(format);
		if(totalMemoryUsage < 0)
			totalMemoryUsage = 0;
		this.width = 0;
		this.height = 0;
		this.format = 0;
		
		if(id != 0) {
			nativeFree(id);
			id = 0;
		}	
		markChanged();
	}
	
	private class WriteCommand extends CustomCommand {
		private int x;
		private int y;
		private int width;
		private int height;
		private int format;
		private int offset;
		private int pitch;
		private Object data;
			
		public WriteCommand(int x, int y, int width, int height, Object data, int offset, int pitch, int format) {
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.data = data;
			this.offset = offset;
			this.pitch = pitch;
			this.format = format;
			CommandBuffer.getInstance().addCommand(this);
		}

		public void execute() {
			if(data instanceof byte[])
				nativeWriteByte(id, x, y, width, height, (byte[])data, offset, pitch, format);
			else if(data instanceof int[])
				nativeWriteInt(id, x, y, width, height, (int[])data, offset, pitch, format);
			else if(data instanceof float[])
				nativeWriteFloat(id, x, y, width, height, (float[])data, offset, pitch, format);
		}		
	}
	
	/**
 	 * Writes the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void write(int x, int y, int width, int height, byte[] data, int format) {
		write(x, y, width, height, data, format, 0, 0);
	}
	
	public void write(int x, int y, int width, int height, byte[] data, int format, int offset, int pitch) {
		new WriteCommand(x, y, width, height, data, offset, pitch, format).waitCompletion();
		markChanged();
	}
	
	/**
 	 * Writes the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void write(int x, int y, int width, int height, int[] data, int format) {
		write(x, y, width, height, data, format, 0, 0);
	}
	
	public void write(int x, int y, int width, int height, int[] data, int format, int offset, int pitch) {
		new WriteCommand(x, y, width, height, data, offset, pitch, format).waitCompletion();
		markChanged();
	}
	
	/**
 	 * Writes the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void write(int x, int y, int width, int height, float[] data, int format) {
		write(x, y, width, height, data, format, 0, 0);
	}
	
	public void write(int x, int y, int width, int height, float[] data, int format, int offset, int pitch) {
		new WriteCommand(x, y, width, height, data, offset, pitch, format).waitCompletion();
		markChanged();
	}
	/**
 	 * Reads the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void read(int x, int y, int width, int height, byte[] data, int format) {
		nativeReadByte(id, x, y, width, height, data, 0, 0, format);
	}
	
	/**
 	 * Reads the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void read(int x, int y, int width, int height, int[] data, int format) {
		nativeReadInt(id, x, y, width, height, data, 0, 0, format);
	}
	
	/**
 	 * Reads the pixel data of the image in the given rectangle.
 	 * 
	 * @param x the left side of the rectangle
	 * @param y the top of the rectangle
	 * @param width the width of the rectangle
	 * @param height the height of the rectangle
	 * @param data the pixel data
	 * @param format format of the pixel data
	 */
	public void read(int x, int y, int width, int height, float[] data, int format) {
		nativeReadFloat(id, x, y, width, height, data, 0, 0, format);
	}
	
	private native int nativeCreate(int id, int width, int height, int format, int options);
	private native void nativeFree(int id);
	private native boolean nativeContentsLost(int id);
	private native void nativeWriteByte(int id, int x, int y, int width, int height, byte[] data, int offset, int pitch, int format);	
	private native void nativeWriteInt(int id, int x, int y, int width, int height, int[] data, int offset, int pitch, int format);	
	private native void nativeWriteFloat(int id, int x, int y, int width, int height, float[] data, int offset, int pitch, int format);	
	private native void nativeReadByte(int id, int x, int y, int width, int height, byte[] data, int offset, int pitch, int format);
	private native void nativeReadInt(int id, int x, int y, int width, int height, int[] data, int offset, int pitch, int format);
	private native void nativeReadFloat(int id, int x, int y, int width, int height, float[] data, int offset, int pitch, int format);

	@Override
	public BufferedImage getSnapshot() {
		return convertToBufferedImage(BufferedImage.TYPE_3BYTE_BGR);
	}

	@Override
	public Graphics2D createGraphics() {
		return new ScenicGraphics2D(CommandBuffer.getInstance().createContext(this));
	}

	@Override
	public int validate(GraphicsConfiguration gc) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public ImageCapabilities getCapabilities() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int getWidth(ImageObserver observer) {
		return width;
	}

	@Override
	public int getHeight(ImageObserver observer) {
		return height;
	}

	@Override
	public Object getProperty(String name, ImageObserver observer) {
		return UndefinedProperty;
	}
}
